#导入必要的库
import base64  # 用于Base64编码和解码，在生成请求签名时处理User-Agent
import hashlib  # 提供哈希算法，如MD5，用于生成请求签名
import math  # 提供数学函数，如ceil用于时间戳处理
import random  # 生成随机数，用于请求签名中的随机参数
import time  # 提供时间相关功能，如获取当前时间戳
import jsonpath  # JSONPath库，用于从复杂JSON结构中提取数据
import pandas as pd  # 强大的数据处理库，用于数据清洗、分析和转换
import requests  # 简洁的HTTP请求库，用于发送网络请求获取数据
import matplotlib.pyplot as plt  # 绘图库，用于数据可视化
import os  # 提供操作系统相关功能，如文件和目录操作
import argparse  # 命令行参数解析库，用于解析用户输入的命令行参数
from datetime import datetime # 日期和时间处理库
import numpy as np  # 科学计算基础库，提供高性能数组操作


class MaoyanBoxOfficeCrawler:
    """猫眼电影票房数据爬虫与分析类
    该类用于从猫眼电影票房网站抓取数据，对数据进行解析、预处理、分析和可视化。
    """

    def __init__(self, output_dir='数据可视化', save_csv=True, save_img=True):
        """初始化爬虫类

        Args:
            output_dir: 输出文件目录，用于保存CSV文件和图片
            save_csv: 是否保存CSV文件，布尔值
            save_img: 是否保存图片，布尔值
        """
        self.output_dir = output_dir
        self.save_csv = save_csv
        self.save_img = save_img
        # 设置请求头，模拟浏览器访问，避免被网站识别为爬虫
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 15_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15',
            'Referer': 'https://piaofang.maoyan.com/dashboard/movie',
            'Cookie': '_lxsdk_cuid=19734bbe29ec8-0363fa7e5db86a-7e433c49-16a7f0-19734bbe29ec8; _lxsdk=FE36D070404D11F08A28BDFC6D5346FD2FA34386CFC24FFB949EC1BB9BE626FD; _ga=GA1.1.702629089.1748936418; _ga_WN80P4PSY7=GS2.1.s1748936418$o1$g0$t1748936420$j58$l0$h0; _lx_utm=utm_source%3Dbing%26utm_medium%3Dorganic; _lxsdk_s=19734bbe29e-0d1-79a-f19%7C%7C5'
        }
        # 定义请求的基础URL
        self.base_url = 'https://piaofang.maoyan.com/dashboard-ajax/movie'
        # 用于存储解析后的数据
        self.data = None

        # 创建输出目录（使用新名称"数据可视化"）
        if not os.path.exists(output_dir):
            os.makedirs(output_dir)

        # 设置中文字体，解决中文显示问题
        plt.rcParams['font.sans-serif'] = ['SimHei', 'WenQuanYi Micro Hei', 'Heiti TC', 'STHeiti']
        # 解决负号显示问题
        plt.rcParams['axes.unicode_minus'] = False

    @staticmethod
    def convert_box_office(value):
        """
        将不同单位和格式的票房数据统一转换为以"万元"为单位的浮点数

        参数:
            value: 原始票房数据（可能为字符串、数字或空值）

        返回:
            float: 转换后的票房数值（万元），无法转换时返回0.0
        """
        # 处理空值或缺失数据
        if pd.isna(value):
            return 0.0

        # 转换为字符串并去除首尾空格
        value = str(value).strip()

        # 处理"亿"为单位的数据（如"5.23亿"转换为52300.0万元）
        if '亿' in value:
            try:
                return float(value.replace('亿', '')) * 10000
            except ValueError:
                return 0.0  # 转换失败时返回0

        # 处理"万"为单位的数据（如"1234万"转换为1234.0万元）
        elif '万' in value:
            try:
                return float(value.replace('万', ''))
            except ValueError:
                return 0.0

        # 处理纯数字字符串（如"12345"直接转换为12345.0万元）
        elif value.isdigit():
            return float(value)

        # 处理以"<"开头的特殊值（如"<0.1亿"转换为1000.0万元）
        elif value.startswith('<'):
            try:
                if '亿' in value:
                    return float(value.replace('<', '').replace('亿', '')) * 10000
                else:
                    return float(value.replace('<', ''))
            except ValueError:
                return 0.0

        # 处理无法识别的格式，统一返回0
        else:
            return 0.0

    def _generate_signature(self):
        """生成请求签名，应对网站反爬机制
        通过对User-Agent进行Base64编码，生成随机数和时间戳，拼接签名内容并进行MD5加密。
        """
        # 对User-Agent进行Base64编码（使用base64库）
        user_agent = self.headers['User-Agent']
        encoded_ua = str(base64.b64encode(user_agent.encode('utf-8')), 'utf-8')

        # 生成随机数（使用random库）和时间戳（使用time库）
        index = str(round(random.random() * 1000))
        times = str(math.ceil(time.time() * 1000))  # 使用math.ceil向上取整

        # 拼接签名内容
        content = "method=GET&timeStamp={}&User-Agent={}&index={}&channelId=40009&sVersion=2&key=A013F70DB97834C0A5492378BD76C53A".format(
            times, encoded_ua, index)

        # 生成MD5签名（使用hashlib库）
        md5 = hashlib.md5()
        md5.update(content.encode('utf-8'))
        sign = md5.hexdigest()

        return {
            'timeStamp': times,
            'User-Agent': encoded_ua,
            'index': index,
            'signKey': sign
        }

    def fetch_data(self):
        """获取猫眼电影票房数据
        生成签名和请求参数，发送HTTP请求获取数据（使用requests库）。
        """
        print("正在获取猫眼电影票房数据...")

        # 生成签名和请求参数
        signature = self._generate_signature()
        params = {
            'orderType': '0',
            'uuid': '17d79b87a00c8-015087c7514df4-5919145b-144000-17d79b87a00c8',
            **signature,
            'channelId': '40009',
            'sVersion': '2'
        }

        # 发送请求（使用requests库的get方法）
        try:
            response = requests.get(url=self.base_url, headers=self.headers, params=params, timeout=15)
            # 检查请求是否成功
            response.raise_for_status()
            data = response.json()
            print("数据获取成功")
            return data
        except requests.exceptions.RequestException as e:
            print(f"请求失败: {e}")
            return None

    def parse_data(self, raw_data):
        """解析原始数据并转换为DataFrame（使用pandas库）

        Args:
            raw_data: 原始JSON数据

        Returns:
            pandas.DataFrame: 解析后的DataFrame
        """
        if not raw_data:
            return None

        print("正在解析数据...")

        # 使用JSONPath（jsonpath库）提取数据
        data_avgSeatView = jsonpath.jsonpath(raw_data, '$..avgSeatView')  # 上座率
        data_avgShowView = jsonpath.jsonpath(raw_data, '$..avgShowView')  # 场均人次
        data_boxRate = jsonpath.jsonpath(raw_data, '$..boxRate')  # 票房占比
        data_name = jsonpath.jsonpath(raw_data, '$..movieName')  # 电影名称
        data_time = jsonpath.jsonpath(raw_data, '$..releaseInfo')  # 上映时间
        data_sumBoxDesc = jsonpath.jsonpath(raw_data, '$..sumBoxDesc')  # 综合票房
        data_showCount = jsonpath.jsonpath(raw_data, '$..showCount')  # 排片场次
        data_showCountRate = jsonpath.jsonpath(raw_data, '$..showCountRate')  # 排片占比

        # 构建DataFrame（使用pandas库）
        data = {
            '电影名称': data_name,
            '上映时间': data_time,
            '上座率': data_avgSeatView,
            '场均人次': data_avgShowView,
            '票房占比': data_boxRate,
            '综合票房': data_sumBoxDesc,
            '排片场次': data_showCount,
            '排片占比': data_showCountRate
        }

        df = pd.DataFrame(pd.DataFrame.from_dict(data, orient='index').values.T, columns=list(data.keys()))

        # 数据预处理
        df = self.preprocess_data(df)
        print("数据解析完成")

        return df

    def preprocess_data(self, df):
        """数据预处理（使用pandas库）

        Args:
            df: 原始DataFrame

        Returns:
            pandas.DataFrame: 预处理后的DataFrame
        """
        if df is None or df.empty:
            return df

        # 处理票房数据，将票房数据转换为以万元为单位的浮点数
        df['综合票房'] = df['综合票房'].apply(self.convert_box_office).astype(float)

        # 处理缺失值，将上映时间的缺失值填充为'未知'
        df['上映时间'] = df['上映时间'].fillna('未知')

        # 处理百分比数据
        for col in ['上座率', '票房占比', '排片占比']:
            # 先将<0.1这样的值替换为0.05（中间值）
            df[col] = df[col].str.replace('<0.1', '0.05') if col in df.select_dtypes(include='object').columns else df[col]
            # 移除百分号并转换为小数
            df[col] = df[col].str.replace('%', '').astype(float) / 100 if col in df.select_dtypes(include='object').columns else df[col]

        # 提取上映天数
        df['上映天数'] = df['上映时间'].apply(self._extract_release_days)

        # 添加电影类型（示例：随机分配，使用random库）
        movie_types = ['动作', '喜剧', '爱情', '科幻', '悬疑', '动画', '纪录片', '其他']
        df['电影类型'] = [random.choice(movie_types) for _ in range(len(df))]

        return df

    def _extract_release_days(self, release_info):
        """从上映时间信息中提取上映天数

        Args:
            release_info: 上映时间信息

        Returns:
            int: 上映天数，无法提取时返回-1
        """
        if pd.isna(release_info) or '上映' not in release_info:
            return -1

        try:
            days_str = release_info.split('上映')[1].strip()
            if '天' in days_str:
                return int(days_str.replace('天', ''))
            return 0  # 刚上映，不足1天
        except:
            return -1

    def save_to_csv(self, df):
        """保存数据到CSV文件（使用新文件夹名称，使用pandas库）

        Args:
            df: 要保存的DataFrame
        """
        if not self.save_csv or df is None or df.empty:
            return

        # 获取当前时间戳（使用datetime库），用于生成文件名
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        csv_path = os.path.join(self.output_dir, f"猫眼电影票房_{timestamp}.csv")
        df.to_csv(csv_path, index=False, encoding='utf-8-sig')
        print(f"数据已保存至: {csv_path}")

    def analyze_data(self, df):
        """数据分析（使用pandas库）

        Args:
            df: 要分析的DataFrame

        Returns:
            dict: 分析结果
        """
        if df is None or df.empty:
            return {}

        print("正在进行数据分析...")

        results = {}

        # 票房分布
        results['票房分布'] = df['综合票房'].describe()

        # 票房排名前十的电影
        results['票房前十'] = df.sort_values('综合票房', ascending=False).head(10)

        # 不同类型电影的票房表现
        genre_boxoffice = df.groupby('电影类型')['综合票房'].sum().sort_values(ascending=False)
        results['类型票房'] = genre_boxoffice

        # 上座率排名前十的电影
        results['上座率前十'] = df.sort_values('上座率', ascending=False).head(10)

        print("数据分析完成")
        return results

    def visualize_data(self, df):
        """数据可视化（使用新文件夹名称，使用matplotlib库）

        Args:
            df: 要可视化的DataFrame
        """
        if not self.save_img or df is None or df.empty:
            return

        print("正在生成可视化图表...")
        # 获取当前时间戳（使用datetime库），用于生成文件名
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')

        # 1. 电影类型分布饼图（美化版）
        self._plot_genre_distribution(df, timestamp)

        # 2. 票房数据排行图
        self._plot_top20_boxoffice(df, timestamp)

        # 3. 上座率排行图
        self._plot_top20_seat_view(df, timestamp)

        print("图表生成完成")

    def _plot_genre_distribution(self, df, timestamp):
        """绘制美化版电影类型分布饼图（使用matplotlib库）"""
        # 统计每种电影类型的数量
        genre_counts = df['电影类型'].value_counts()

        # 定义更美观的颜色方案（渐变色，使用matplotlib.colors库）
        colors = plt.cm.viridis(np.linspace(0, 1, len(genre_counts)))

        # 创建画布和子图
        fig, ax = plt.subplots(figsize=(12, 10), subplot_kw=dict(aspect="equal"))

        # 绘制饼图
        wedges, texts, autotexts = ax.pie(
            genre_counts,
            autopct=lambda p: f"{p:.1f}%\n({int(p * sum(genre_counts) / 100)})",
            explode=[0.05] + [0.02] * (len(genre_counts) - 1),  # 突出显示最大的部分
            labels=genre_counts.index,
            startangle=90,
            textprops=dict(color="w", fontsize=12),
            colors=colors,
            shadow=True,
            wedgeprops=dict(width=0.7, edgecolor='w', linewidth=2)  # 创建环形图效果
        )

        # 设置标签和百分比文本样式
        plt.setp(autotexts, size=12, weight="bold")
        plt.setp(texts, size=14)

        # 添加中心标题
        plt.text(0, 0, f"电影类型分布\n共{sum(genre_counts)}部",
                 horizontalalignment='center',
                 verticalalignment='center',
                 fontsize=16,
                 fontweight='bold')

        # 添加图例
        ax.legend(wedges, genre_counts.index,
                  title="电影类型",
                  loc="center left",
                  bbox_to_anchor=(1, 0, 0.5, 1),
                  fontsize=12)

        # 添加外圈装饰
        centre_circle = plt.Circle((0, 0), 0.25, fc='white', ec='gray', linewidth=1.5)
        fig.gca().add_artist(centre_circle)

        # 设置标题
        plt.title("猫眼电影类型分布", fontsize=18, fontweight='bold', pad=20)

        # 调整布局
        plt.tight_layout()

        # 保存图片到"数据可视化"文件夹（使用os库）
        img_path = os.path.join(self.output_dir, f'电影类型分布_{timestamp}.png')
        plt.savefig(img_path, dpi=300, bbox_inches='tight')
        plt.close()

    def _plot_top20_boxoffice(self, df, timestamp):
        """绘制票房前20名电影排行榜（使用matplotlib库）"""
        # 按综合票房降序排序，取前20条数据
        df_top20 = df.sort_values('综合票房', ascending=False).head(20)

        plt.figure(figsize=(12, 10))

        # 创建渐变色（使用numpy和matplotlib库）
        colors = plt.cm.viridis(np.linspace(0.2, 0.9, len(df_top20)))

        # 绘制水平柱状图
        bars = plt.barh(df_top20['电影名称'], df_top20['综合票房'] / 10000, color=colors)

        # 添加票房数值标签
        for bar in bars:
            width = bar.get_width()
            plt.text(width, bar.get_y() + bar.get_height() / 2,
                     f'{width:.2f}亿', ha='left', va='center', fontsize=10, fontweight='bold')

        # 添加网格和标题
        plt.xlabel('综合票房（亿元）', fontsize=14)
        plt.ylabel('电影名称', fontsize=14)
        plt.title('猫眼电影综合票房排行榜', fontsize=18, fontweight='bold')
        plt.yticks(fontsize=12)
        plt.subplots_adjust(left=0.35, right=0.9, top=0.9, bottom=0.1)
        plt.gca().invert_yaxis()  # 票房最高的放在顶部
        plt.grid(axis='x', linestyle='--', alpha=0.7)

        # 保存图片（使用os库）
        img_path = os.path.join(self.output_dir, f'票房排行榜_{timestamp}.png')
        plt.savefig(img_path, dpi=300, bbox_inches='tight')
        plt.close()

    def _plot_top20_seat_view(self, df, timestamp):
        """绘制上座率前20名电影排行榜（使用matplotlib库）"""
        # 按上座率降序排序，取前20条数据
        df_top20 = df.sort_values('上座率', ascending=False).head(20)

        plt.figure(figsize=(12, 10))

        # 创建渐变色（使用numpy和matplotlib库）
        colors = plt.cm.plasma(np.linspace(0.2, 0.9, len(df_top20)))

        # 绘制水平柱状图
        bars = plt.barh(df_top20['电影名称'], df_top20['上座率'] * 100, color=colors)

        # 添加上座率数值标签
        for bar in bars:
            width = bar.get_width()
            plt.text(width, bar.get_y() + bar.get_height() / 2,
                     f'{width:.1f}%', ha='left', va='center', fontsize=10, fontweight='bold')

        # 添加网格和标题
        plt.xlabel('上座率 (%)', fontsize=14)
        plt.ylabel('电影名称', fontsize=14)
        plt.title('猫眼电影上座率排行榜', fontsize=18, fontweight='bold')
        plt.yticks(fontsize=12)
        plt.subplots_adjust(left=0.35, right=0.9, top=0.9, bottom=0.1)
        plt.gca().invert_yaxis()  # 上座率最高的放在顶部
        plt.grid(axis='x', linestyle='--', alpha=0.7)

        # 保存图片（使用os库）
        img_path = os.path.join(self.output_dir, f'上座率排行榜_{timestamp}.png')
        plt.savefig(img_path, dpi=300, bbox_inches='tight')
        plt.close()

    def run(self):
        """运行爬虫和分析流程
        依次执行数据获取、解析、保存、分析和可视化操作。
        """
        raw_data = self.fetch_data()
        if not raw_data:
            print("数据获取失败，程序退出")
            return

        df = self.parse_data(raw_data)
        self.data = df

        if self.save_csv:
            self.save_to_csv(df)

        analysis_results = self.analyze_data(df)
        self.print_analysis_results(analysis_results)

        if self.save_img:
            self.visualize_data(df)

        return df

    def print_analysis_results(self, results):
        """打印分析结果

        Args:
            results: 分析结果字典
        """
        if not results:
            return

        print("\n===== 数据分析结果 =====")

        # 票房分布
        print("\n1. 票房分布:")
        print(results['票房分布'])

        # 票房前十
        print("\n2. 票房前十的电影:")
        for i, (_, row) in enumerate(results['票房前十'].iterrows(), 1):
            print(f"{i}. {row['电影名称']}: {row['综合票房'] / 10000:.2f} 亿元")

        # 类型票房
        print("\n3. 不同类型电影的总票房:")
        for genre, boxoffice in results['类型票房'].items():
            print(f"{genre}: {boxoffice / 10000:.2f} 亿元")

        # 上座率前十
        print("\n4. 上座率前十的电影:")
        for i, (_, row) in enumerate(results['上座率前十'].iterrows(), 1):
            print(f"{i}. {row['电影名称']}: {row['上座率'] * 100:.1f}%")

        print("\n======================\n")


def main():
    """主函数
    解析命令行参数（使用argparse库），创建爬虫实例并运行，提供额外的交互式分析功能。
    """
    # 创建命令行参数解析器
    parser = argparse.ArgumentParser(description='猫眼电影票房数据爬虫与分析')
    parser.add_argument('--output', '-o', default='数据可视化', help='输出目录')
    parser.add_argument('--no-csv', action='store_true', help='不保存CSV文件')
    parser.add_argument('--no-img', action='store_true', help='不保存图片')

    # 解析命令行参数
    args = parser.parse_args()

    # 创建爬虫实例并运行
    crawler = MaoyanBoxOfficeCrawler(
        output_dir=args.output,
        save_csv=not args.no_csv,
        save_img=not args.no_img
    )

    # 运行爬虫
    df = crawler.run()

    # 示例：额外的交互式分析
    if df is not None and not df.empty:
        print("是否需要进行额外的交互式分析? (y/n)")
        if input().lower() == 'y':
            print("\n可用的分析命令:")
            print("1. top_box [n]: 显示票房前n名的电影")
            print("2. top_seat [n]: 显示上座率前n名的电影")
            print("3. genre: 按电影类型统计票房")
            print("4. exit: 退出")

            while True:
                command = input("\n输入命令 (输入 'help' 显示帮助): ").strip().lower()

                if command == 'exit':
                    break
                elif command == 'help':
                    print("\n可用的分析命令:")
                    print("1. top_box [n]: 显示票房前n名的电影")
                    print("2. top_seat [n]: 显示上座率前n名的电影")
                    print("3. genre: 按电影类型统计票房")
                    print("4. help: 显示帮助")
                    print("5. exit: 退出")
                elif command.startswith('top_box'):
                    try:
                        n = int(command.split()[1]) if len(command.split()) > 1 else 10
                        top_movies = df.sort_values('综合票房', ascending=False).head(n)
                        print(f"\n票房前{n}名的电影:")
                        for i, (_, row) in enumerate(top_movies.iterrows(), 1):
                            print(f"{i}. {row['电影名称']}: {row['综合票房'] / 10000:.2f} 亿元 ({row['上映时间']})")
                    except:
                        print("命令格式错误，请使用 'top_box [n]'")
                elif command.startswith('top_seat'):
                    try:
                        n = int(command.split()[1]) if len(command.split()) > 1 else 10
                        top_movies = df.sort_values('上座率', ascending=False).head(n)
                        print(f"\n上座率前{n}名的电影:")
                        for i, (_, row) in enumerate(top_movies.iterrows(), 1):
                            print(f"{i}. {row['电影名称']}: {row['上座率'] * 100:.1f}% ({row['上映时间']})")
                    except:
                        print("命令格式错误，请使用 'top_seat [n]'")
                elif command == 'genre':
                    genre_stats = df.groupby('电影类型')['综合票房'].agg(['sum', 'count', 'mean'])
                    genre_stats.columns = ['总票房(万元)', '电影数量', '平均票房(万元)']
                    genre_stats = genre_stats.sort_values('总票房(万元)', ascending=False)
                    genre_stats['总票房(亿元)'] = genre_stats['总票房(万元)'] / 10000
                    genre_stats['平均票房(亿元)'] = genre_stats['平均票房(万元)'] / 10000
                    print("\n按电影类型统计票房:")
                    print(genre_stats[['总票房(亿元)', '电影数量', '平均票房(亿元)']].to_string(float_format='%.4f'))
                else:
                    print("未知命令，请输入 'help' 查看可用命令")


if __name__ == '__main__':
    main()